{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "3d713b7d",
   "metadata": {},
   "source": [
    "## 调研员：从网络进行搜索并总结报告\n",
    "\n",
    "### 背景\n",
    "在MetaGPT中，调研员角色，可以根据用户的调研问题，从互联网上进行搜索总结，并最终生成报告。本文将从设计思路、代码实现、使用示例等几个方面介绍调研员角色。\n",
    "\n",
    "### 目标\n",
    "通过本文档，你可以了解如何使用MetaGPT的调研员角色进行网络搜索并总结报告，进一步地，可以使用MetaGPT的联网功能开发新的智能体\n",
    "\n",
    "### 设计思路\n",
    "在使用MetaGPT开发Researcher角色之前，我们需要先思考一下假如自己作为一个Researcher，在网络上搜索并输出调研报告是怎么做的。一般是包含以下几个步骤：\n",
    "\n",
    "1. 分析待研究的问题，并将问题拆分成几个可以适合用搜索引擎进行搜索的子问题\n",
    "2. 通过搜索引擎搜索子问题，浏览搜索引擎会给出多个带有标题、原文Url、摘要等信息的搜索结果，判断每一条搜索结果是否与要搜索的问题相关以及信息来源是否可靠，从而选择是否要进一步通过Url浏览网页\n",
    "3. 点击需要进一步浏览的网页，判断网页内容对待研究的问题是否有帮助，提取有关的信息并记录\n",
    "4. 聚合所有的记录下来的相关资料，针对待研究的问题撰写报告\n",
    "因此，我们尝试让GPT模拟以上的调研流程，整体步骤如下：\n",
    "\n",
    "1. 用户输入待调研问题\n",
    "2. 调研员通过GPT生成一组研究问题，这些问题共同形成对任何给定任务的客观意见\n",
    "3. 调研员在收到GPT分解的问题后，对于每个研究问题，先通过搜索引擎搜索，获取初次搜索结果\n",
    "4. 网址通过浏览器获取网页内容，并使用对网页内容进行总结\n",
    "5. 汇总所有总结后的内容并跟踪其来源\n",
    "6. 最后，让GPT根据汇总后的内容生成最终研究报告\n",
    "\n",
    "针对以上的流程，我们可以抽象出3个Action，定义一个Role，如下：\n",
    "\n",
    "\n",
    "| 名称                         | 类型   | 说明                                         |\n",
    "|--------------------------|------|------------------------------------------|\n",
    "| CollectLinks                 | Action | 从搜索引擎进行搜索，并获取Url地址列表                  |\n",
    "| WebBrowseAndSummarize | Action | 浏览网页并总结网页内容                           |\n",
    "| ConductResearch           | Action | 生成调研报告                                   |\n",
    "| Researcher                 | Role  | 调研员智能体，从网络进行搜索并总结报告                   |\n",
    "\n",
    "\n",
    "所用的配置信息如下：\n",
    "config/config2.yaml\n",
    "```yaml\n",
    "llm:\n",
    "  api_type: 'ollama'\n",
    "  base_url: 'http://192.168.0.123:11434/api'\n",
    "  model: 'qwen2.5:7b'\n",
    "  max_token: 4096\n",
    "  \n",
    "repair_llm_output: true\n",
    "\n",
    "embedding:\n",
    "  api_type: 'ollama'\n",
    "  base_url: 'http://192.168.0.123:11434/api'\n",
    "  model: 'qwen2.5:7b'\n",
    "\n",
    "browser:\n",
    "  engine: 'selenium'\n",
    "  browser_type: 'chrome'\n",
    "```"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 46,
   "id": "f3d1d0ce",
   "metadata": {},
   "outputs": [],
   "source": [
    "%%bash\n",
    "mkdir -p config  # 创建 config 目录（若不存在）\n",
    "cat <<EOF > config/config2.yaml\n",
    "llm:\n",
    "  api_type: 'zhipuai'\n",
    "  api_key: '2ce2c9417f83dc0f582c00498b2bbd8b.374mpCJDIXF5ksmq'\n",
    "  model: 'glm-4-flash'\n",
    "  \n",
    "repair_llm_output: true\n",
    "\n",
    "embedding:\n",
    "  api_type: 'zhipuai'\n",
    "  api_key: '2ce2c9417f83dc0f582c00498b2bbd8b.374mpCJDIXF5ksmq'\n",
    "  model: 'glm-4-flash'\n",
    "\n",
    "browser:\n",
    "  engine: 'selenium'\n",
    "  browser_type: 'chrome'\n",
    "EOF"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6c8a4e37",
   "metadata": {},
   "source": [
    "由于metaGPT非常消耗token，所以我们用本地大模型qwen2.5:7b。然后搜索引擎我们稍后自己定制，不从配置导入，所以没有配置search。浏览器工具用selenium\n",
    "所有的代码建议在jupyter notebook 中运行，这样可以随时看到一步步的输出结果，方便学习。\n",
    "\n",
    "\n",
    "Action定义\n",
    "\n",
    "CollectLinks\n",
    "\n",
    "CollectLinks Action用于从搜索引擎搜索相关问题并获取Url地址列表。因为用户输入的问题不一定适合直接通过搜索引擎搜索，因此在进行搜索之前，先将用户输入的问题拆分成多个适合搜索的子问题，然后通过搜索引擎搜索这些子问题，筛选出与调研问题有关的Url，并根据网站可靠性对url列表进行排序。\n",
    "\n",
    "metaGPT提供的几个搜索引擎都是国外的，必须要梯子，其实我们可以直接用智谱的，下面自定义一个智谱的搜索工具。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 61,
   "id": "668cb674",
   "metadata": {},
   "outputs": [],
   "source": [
    "%%bash\n",
    "cat <<EOF > .env\n",
    "ZHIPU_API_KEY=<YOUR API KEY>\n",
    "EOF"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 62,
   "id": "86fd7f51",
   "metadata": {},
   "outputs": [],
   "source": [
    "import os\n",
    "from dotenv import load_dotenv\n",
    "\n",
    "# 加载环境变量\n",
    "load_dotenv()\n",
    "# 从环境变量中读取api_key\n",
    "\n",
    "from openai import OpenAI\n",
    "from datetime import datetime\n",
    "def zhipu_web_search_tool(query: str) -> str:\n",
    "    \"\"\"\n",
    "    使用智谱AI的GLM-4模型进行联网搜索，返回搜索结果的字符串。\n",
    "    \n",
    "    参数:\n",
    "    - query: 搜索关键词\n",
    "\n",
    "    返回:\n",
    "    - 搜索结果的字符串形式\n",
    "    \"\"\"\n",
    "    # 初始化客户端\n",
    "    client = OpenAI(\n",
    "        api_key=os.getenv('ZHIPU_API_KEY'),\n",
    "        base_url=\"https://open.bigmodel.cn/api/paas/v4/\"\n",
    "    ) \n",
    "    success = False\n",
    "\n",
    "    # 获取当前日期\n",
    "    current_date = datetime.now().strftime(\"%Y-%m-%d\")\n",
    "\n",
    "    print(\"current_date:\", current_date)\n",
    "    \n",
    "    # 设置工具\n",
    "    tools = [{\n",
    "        \"type\": \"web_search\",\n",
    "        \"web_search\": {\n",
    "            \"enable\": True,\n",
    "            \"search_result\": True\n",
    "        }\n",
    "    }]\n",
    "\n",
    "    # 系统提示模板，包含时间信息\n",
    "    system_prompt = f\"\"\"你是一个具备网络访问能力的智能助手，在适当情况下，优先使用网络信息（参考信息）来回答，\n",
    "    以确保用户得到最新、准确的帮助。当前日期是 {current_date}。\"\"\"\n",
    "        \n",
    "    # 构建消息\n",
    "    messages = [\n",
    "        {\"role\": \"system\", \"content\": system_prompt},\n",
    "        {\"role\": \"user\", \"content\": query}\n",
    "    ]\n",
    "        \n",
    "    # 调用API\n",
    "\t# 因为智谱返回的结果有时候没有web_search，所以我们可以让它反复生成，直到生成web_search为止\n",
    "    while success == False:\n",
    "        response = client.chat.completions.create(\n",
    "            model=\"glm-4-flash\",\n",
    "            messages=messages,\n",
    "            tools=tools\n",
    "        )\n",
    "        try:\n",
    "            rst = response.web_search\n",
    "            success = True\n",
    "        except:\n",
    "            pass\n",
    "        \n",
    "    \n",
    "    # 返回结果\n",
    "    return rst"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "faa53bda",
   "metadata": {},
   "source": [
    "可以试一试这个搜索引擎接口好不好用？"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 64,
   "id": "3587d5eb",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "current_date: 2025-02-26\n",
      "[{'content': '吴说获悉，Solana 宣布首届全球线上 AI 黑客松正式启动，将于 12 月 10 日至 23 日举行，奖金总额超 18.5 万美元，设立六大赛道聚焦 AI 代理开发及基础设施建设等创新领域。主赛道由 a16z crypto 和 Solana Foundation 支持，奖金最高 3 万美元，其余赛道涵盖 AI 基础设施、代币工具、自治聊天代理、Social/影响力者代理等，每个赛道由行业合作伙伴提供支持。参赛无需注册，作品提交将于 12 月 15 日开始。Solana 提供团队匹配、导师指导和资源支持，激励开发者构建基于 Solana 的 AI 创新应用。\\n（转自：吴说）', 'icon': '', 'link': 'https://finance.sina.com.cn/blockchain/roll/2024-12-11/doc-incyzzcx0976575.shtml', 'media': '新浪财经', 'refer': 'ref_1', 'title': 'Solana 宣布首届全球线上 AI 黑客松正式启动'}, {'content': '本文来自：SendAI 负责人 Yash Agarwal\\n编译Odaily星球日报\\n译者Azuma\\n编者按：由 SendAI 主办的 Solana AI 黑客松启动已有一定时日，但由于 AI 叙事的持续升温，社区对于相关项目的追捧热度丝毫不减。\\n链上分析研究机构 Pine Analytics 昨日曾统计表示，由该场黑客松参与项目创建或直接关联的代币已多达 65 个，总市值为 4.82 亿美元，平均市值为 750 万美元，市值中位数为 170 万美元；有 13 个代币的市值超过 1000 万美元， 42 个代币的市值超过 100 万美元。\\n然而，在社区情绪如此激昂的情况下，SendAI 创始人 Yash Agarwal 今日却发布了一篇长文抨击当前的炒作乱象 —— Yash 强调过去一周黑客松主页仅有 4 万次访问，但随随便便一个代币都能速通 5000 万市值SendAI 本想构建一个助推 Solana 生态 AI 发展的平台，但现在却成为了堕落者们的赌场”。在 Yash 来看，当前的泡沫会造成开发者趋向于盲目、短时，不利于 Solana AI 的长线发展，因此有必要指出一些荒谬的状况，否则不久之后行业就会看到一场大灾难”。\\n以下为 Yash 全文内容，由 Odaily 星球日报编译。\\n当我们启动这场黑客松时，我有一个简单的愿景 —— 让 Solana 成为 AI 区块链，并实现 10 倍增长。\\n我认为黑客松是实现这一目标的最佳可行途径，我们可以将所有人聚集在同一屋檐下，我个人也可以投资那些非常看好的 Crypto x AI Agent 项目。\\n在过去的 1 个月里，我可以说 Solana AI 黑客松不仅已打响了品牌，且已完全改变了 Solana 生态 AI Agent 的格局。在 15 天内完成 400 多个项目绝非易事，我和我的团队对于为 Solana AI 生态增加 5 - 10 亿美元的市值而做的贡献感到无比自豪。\\n当然了，当我们启动这场黑客松时就曾有预感，由于这种奇怪的 Crypto x AI Agent 模型，会有一些相关代币发行。如果开发者是在利用代币作为资本积累手段，目的仍是构建出色的产品，那完全没有问题。我们还精心设计了所有 8 个赛道（赞助商对此没有发言权），以鼓励真正有用的 AI Agent 。\\n但我们没想到，它会变成堕落者们的赌场”。为了让大家对数字有更清晰的概念，Solana AI 黑客松的项目主页在过去一周只有超过 4 万名访客，', 'icon': '', 'link': 'https://finance.sina.com.cn/blockchain/roll/2025-01-06/doc-ineczakc2664172.shtml', 'media': '新浪财经', 'refer': 'ref_2', 'title': 'Solana AI黑客松“金狗”不断，主办方疾呼开发者停手'}, {'content': '如今正值技术变革的重要时刻，为了让更多的开发者拥抱 AI浪潮，学习和参与 AI开发与应用，腾讯云联合全国知名高校、热门技术社区重磅发起「AI开发黑客松」，围绕AI开发展开编程挑战赛、技术训练营、线上直播等技术实践系列活动。\\n本期腾讯云 AI 代码助手编程挑战赛作为「AI 开发黑客松」系列活动之一，以用AI构造AI——打造属于个人的Copilot”为题，聚焦腾讯云AI辅助编程工具的实战能力。你将使用腾讯云AI代码助手的强大辅助编程能力，尝试结合大模型与对话组件搭建出一个AI对话机器人的网页。你可借助腾讯云AI代码助手的强大能力完成赛题编码与作品发布，了解如何在编码过程中进行代码学习、代码设计优化、代码单元测试，实现编码提速！\\n快来加入「AI 开发黑客松」腾讯云AI代码助手编程挑战赛，动手实践吧！\\n一、两大重磅奖项，众多丰厚奖励\\n本次编程挑战赛分设优秀编程作品奖”与优秀内容创作奖”两个奖项。不论你是有好的代码创意，还是能够输出优秀作品内容，都有机会在本次比赛中突出重围，收获大奖！\\n优秀编程作品奖\\n一等奖（1名）：12000元\\n二等奖（2名）：8000元\\n三等奖（3名）：2000元\\n优秀内容创作奖\\n创作大师奖（1名）：6000元\\n创作精英奖（2名）：2000元\\n创作能手奖（3名）：800元\\n创作新星奖（10名）：300元\\n除奖金外，你还有机会获得：\\n腾讯云AI代码助手专业版：针对优秀编程作品奖及优秀内容创作奖获奖者，每人奖励腾讯云AI代码助手专业版（一年期）。（如果同时获得优秀编程作品奖及优秀内容创作奖，不重复计算）\\n腾讯周边：符合要求的前80名作品提交者，每人一个腾讯公仔。\\n专业开发者活跃交流圈：开发者一起学习交流、前沿信息共享、创新合作等；\\n流量扶持：优秀作品将有可能获得CSDN官方的流量扶持，提升个人影响力；\\n行业大咖指导及技术经验分享：腾讯云资深专家的技术及经验分享，专业指导和专家答疑等。\\n二、创意无限，等你来战\\n1、参赛人员\\n面向全社会开放。高等院校在校生、科研单位工作者、企业人员等均可以报名参加。\\n2、时间安排\\n2024年12月1日-2025年1月17日：线上报名及作品提交\\n2025年1月17日-2025年1月20日：线上评审\\n2025年1月21日：结果公布\\n三、赛前公开课不要错过！\\n为了帮助大家更好地备战，我们贴心准备了线上赛前公开课，为大家讲解如何利用腾讯云AI代码助手进行辅助编程，帮助大家顺利通关，勇夺佳绩！', 'icon': '', 'link': 'https://blog.csdn.net/csdnstudent/article/details/144554007', 'media': 'CSDN博客', 'refer': 'ref_3', 'title': '超级「码」力大PK！腾讯云 AI 代码助手编程挑战赛等你来战'}, {'content': '一、背景介绍\\n首先介绍一下参加这次【 NVIDIA NIM 黑客松训练营】的背景。\\n本人是一名 11 年的互联网开发老鸟了，从 Web 后端开发，到全栈开发，再到后拉的物联网开发，以及目前的研发管理，10 多年一步步见证了互联网焕然一新的快速变革之路，也就是从去年开始接触 AIGC、AI Agent 等相关的一些技术，也几乎体验了国内主流的大多数 AI 产品（看我以前的博文就可以看到 22 年初就体验【华为云 ModelArts】并且撰写了当时体验的过程）\\n但是随着 23 年 AI 的迅速发展和崛起，既出现了非常多的广泛 AI 应用场景、也改变了不少行业的工作方式和发展方向，本来作为一个老程序员，内心其实对新技术还是有一点点排斥的，但是我也知道，AI 这个技术，特别是做互联网开发的人，根本躲不过去的。所以这两年我主要业余精力都有放在 AI 和 Web3 领域；今年年初换购了一个 M3 MacBook Pro 其实也是想更多的研究这两个方面的技术（不要让硬件拖我后腿）。\\n然后参加这次【 NVIDIA NIM 黑客松训练营】也是看到有技术群里在讨论这个活动，周末抽空关注了一下，感觉挺有意思，把错过了的之前直播视频回放也恶补了一下。恶补完成后，就迫不及待的拿到相关训练营资料就直接开干了。\\n好了，废话不多说了，直接进入详细的 WorkShop 环节。\\n二、活动介绍\\n首先活动的形式是纯线上，以训练营学习的方式，再到动手实践，期间提供了非常详细的学习资料，让初次接受 NVIDIA NIM 微服务产品的开发者能够少走很多弯路。\\n其次，在资料介绍中，也提供了比较丰富多样的AI场景及模型教程，这样可以覆盖大多数开发者想实现的AI场景。\\n大语言模型 Phi-3 LLMs\\n生成代码 Granite Code\\n生成文本向量 Generate Embeddings for Text Retrieval （agent-blueprints）\\n视觉语义分割ChangeNet\\n光学字符检测OCDRNet\\n统计图表理解deplot（agent-blueprints）\\n并且还提供了一个较为完整的案例。\\n基于NVIDIA NIM 平台的知识问答系统实验手册\\n总之，从训练营准备工作来说，已经非常全面了，这样的训练营可以覆盖大多数的开发者进行友好的体验。\\n为了活动的趣味性和丰富性，活动方还准备了丰厚的活动激励，可以说是深得人心了，这下不支持都不行了，话不多说，', 'icon': '', 'link': 'https://blog.csdn.net/Mr_Roki/article/details/143468266', 'media': 'CSDN博客', 'refer': 'ref_4', 'title': '【NVIDIA NIM 黑客松训练营】实现剧本杀 AI 策划官（手摸手教程）'}, {'content': '作者：赵甜怡\\n编辑：林炯佳\\n2024年7月的一个晚上，在杭州湖畔创研中心的一个封闭会堂中人头攒动。\\n台下，二百多名年轻黑客席地而坐，随意地三两交谈。在他们之中，有的是顶刊AI论文的作者，有的是独立开发者，还有的是知名创业者此刻，他们正在期待着比赛的开始。\\n突然，全场熄灯，舞台灯光聚焦台上。\\n即将上台的Ryan紧张不已。还在上高二的他马上要在二百多位精英人才的注视下发言。\\n但这次他并不是来展示自己的项目，而是作为活动的创始人为开幕式致辞：\\n欢迎大家来到AdventureX——属于中国年轻人的第一场黑客松！”Ryan说罢，台下掌声雷动。\\n黑客松最早可以追溯到20世纪60年代，黑客亚文化席卷了现在闻名天下的硅谷。\\n打破传统的创新精神与计算机的技术力量相结合，组成了早期硅谷文化的核心。苹果创始人乔布斯也深受其影响。\\n半个多世纪过去，创新的火焰以活动的形式照亮着越来越多有理想的年轻人。\\n活动的类型有很多，主要是由科技公司举办的商业黑客松，以及高校、社群、公益组织举办的技术黑客松。\\n自从ChatGPT打响了新一轮的人工智能竞赛之后，全球黑客松的数量显著增加，越来越多社区和公益组织开始筹办相关活动。\\nAdventureX逃逸计划就是其一。\\n活动主题的别出心裁、创始团队的亮眼背景、参赛者的精彩表现都让这场活动成为了中国科技创投史上一个里程碑式的存在。\\nRyan介绍道，AdventureX是一场为期五天的黑客松活动，希望让更多中国年轻人为改变世界而活。\\n在活动现场，参赛者们自行组队，以团队的方式进行72小时的高强度产品开发，完成从头脑风暴、原型设计到代码编写和项目展示的全过程。\\n表现优秀的团队将有机会直接获得来自企业代表的大额奖金，以及基金投资人的天使轮资金。\\n年轻活力是AdventureX的底色。Ryan也为活动定下了准入门槛：最大参加年龄为26岁。\\n苛刻的门槛仍吸引了1200余名优秀人才争先报名。让人惊讶的是，从最初的想法到活动开始，只有4个月的筹备时间，创始团队也大部分都是高中生和大学生。\\n他们究竟是如何完成这场出彩的活动？\\nPConline与AdventureX创始人Ryan展开了深入对话。\\n第一个投资人很重要。”Ryan在大洋彼岸，顶着12小时的时差分享道。\\nRyan的思路是找到第一个有名气的机构投资——在圈子内建立信用度（Credibility）——利用信用度做宣传（Marketing），吸引更多的参赛者——利用参赛者的资源反向说服投资。', 'icon': '', 'link': 'https://tech.ifeng.com/c/8fetq46KTs1', 'media': '凤凰网科技', 'refer': 'ref_5', 'title': '年薪百万招不到人，AI工程师都去哪了？'}]\n"
     ]
    }
   ],
   "source": [
    "rsp = zhipu_web_search_tool(\"最近三个月有哪些AI黑客松？\")\n",
    "print(rsp)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a35e2bc7",
   "metadata": {},
   "source": [
    "\n",
    "然后我们可以把上面智谱这个搜索函数封装成metaGPT可以用的工具"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 75,
   "id": "e9958092",
   "metadata": {},
   "outputs": [],
   "source": [
    "from __future__ import annotations\n",
    "\n",
    "import asyncio\n",
    "import json\n",
    "from concurrent import futures\n",
    "from typing import Literal, Optional\n",
    "from pydantic import BaseModel, ConfigDict\n",
    "class ZhipuAPIWrapper(BaseModel):\n",
    "    model_config = ConfigDict(arbitrary_types_allowed=True)\n",
    "\n",
    "    loop: Optional[asyncio.AbstractEventLoop] = None\n",
    "    executor: Optional[futures.Executor] = None\n",
    "\n",
    "    async def run(\n",
    "        self,\n",
    "        query: str,\n",
    "        max_results: int = 5,\n",
    "        as_string: bool = True,\n",
    "    ) -> str | list[dict]:\n",
    "        \"\"\"Return the results of a Zhipu search using the official Google API\n",
    "\n",
    "        Args:\n",
    "            query: The search query.\n",
    "            max_results: The number of results to return.\n",
    "            as_string: A boolean flag to determine the return type of the results. If True, the function will\n",
    "                return a formatted string with the search results. If False, it will return a list of dictionaries\n",
    "                containing detailed information about each search result.\n",
    "\n",
    "        Returns:\n",
    "            The results of the search.\n",
    "        \"\"\"\n",
    "        loop = self.loop or asyncio.get_event_loop()\n",
    "        future = loop.run_in_executor(\n",
    "            self.executor,\n",
    "            self._search_from_zhipu,\n",
    "            query,\n",
    "            max_results,\n",
    "        )\n",
    "        search_results = await future\n",
    "        if as_string:\n",
    "            return json.dumps(search_results, ensure_ascii=False)\n",
    "        return search_results\n",
    "\n",
    "    def _search_from_zhipu(self, query: str, max_results: int):\n",
    "        return [\n",
    "            {\"link\": i[\"link\"], \"snippet\": i[\"content\"], \"title\": i[\"title\"]}\n",
    "            for (_, i) in zip(range(max_results), zhipu_web_search_tool(query))\n",
    "        ]"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c70b7475",
   "metadata": {},
   "source": [
    "上面这些代码是根据源代码 https://github.com/geekan/MetaGPT/blob/main/metagpt/tools/search_engine_ddg.py改写的，其实就是把里面的\n",
    "self.ddgs.text(query) 换成了 zhipu_web_search_tool(query) 而已。\n",
    "\n",
    "我们来试试这个封装起来的函数好不好用"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 66,
   "id": "bef40383",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "current_date: 2025-02-26\n",
      "3\n"
     ]
    }
   ],
   "source": [
    "run_func = ZhipuAPIWrapper().run\n",
    "found = await run_func(\"上海最近三个月有哪些AI会议？\", max_results=3, as_string=False)\n",
    "print(len(found))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9a9c83c0",
   "metadata": {},
   "source": [
    "结果会显示 3\n",
    "就是找到三条记录，其中每一条记录都是像下面这样的字典：\n",
    "{\n",
    "\t'link': 'https://zhengce.qizhidao.com/article-zcview/707160600044830720.html', \n",
    "\t'snippet': '知识产权是企业的核心竞争力之一，专利授权是其中最重要的组成部分之一。在拥有大量专利的情况下，企业能够更好地保护其研发成果和技术优势，从而提高市场份额和盈利能力。因此，专利授权率对申报知识产权优势企业的影响至关重要。\\n专利授权率可以直接反映出企业的创新能力和技术实力。专利授权率越高，说明企业的创新成果越多，技术实力越强。这种技术实力是企业在竞争中不可或缺的优势，可以吸引更多的顶尖人才、合作伙伴和投资者，进一步促进企业的发展。\\n专利授权率也可以影响到企业的商业模式和市场准入。在某些行业中，专利的重要性更加突出。例如，在药品制造业中，拥有专利的企业可以获得较长时间的独家生产权，从而保持市场优势。同样，在电子产品制造业中，拥有核心专利的企业可以控制市场标准，进一步增强市场地位。因此，专利授权率越高，企业在市场准入和商业模式方面的优势也就越大。\\n专利授权率还可以影响到企业的资本运作。在融资过程中，拥有高专利授权率的企业通常更受投资者欢迎。这是因为专利授权率反映了企业的技术实力和创新能力，并且与企业的盈利能力直接相关。高专利授权率意味着企业拥有更多的收入来源和更强的竞争力，从而更容易获得资本市场的融资支持。\\n综上所述，专利授权率对申报知识产权优势企业的影响非常重要。高专利授权率可以直接反映企业的创新能力和技术实力，进一步影响到企业的商业模式、市场准入和资本运作。因此，在知识产权战略规划中，提高专利授权率是企业应该考虑的重要因素之一，可以帮助企业获得更大的竞争优势。', \n",
    "\t'title': '企业的专利授权率对申报知识产权优势企业有影响吗？-企知道'\n",
    "}\n",
    "每条数据都有link、snippet、title三个信息。\n",
    "\n",
    "这个搜索工具现在是很好用的了，下面我们来构建Action。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 82,
   "id": "447f8339",
   "metadata": {},
   "outputs": [],
   "source": [
    "from __future__ import annotations\n",
    "\n",
    "import asyncio\n",
    "from typing import Any, Callable, Optional, Union\n",
    "from metagpt.logs import logger\n",
    "from metagpt.config2 import config\n",
    "from pydantic import TypeAdapter, model_validator\n",
    "from metagpt.actions import Action\n",
    "from metagpt.tools.search_engine import SearchEngine\n",
    "from metagpt.utils.common import OutputParser\n",
    "from metagpt.utils.text import generate_prompt_chunk, reduce_message_length\n",
    "\n",
    "LANG_PROMPT = \"请用{language}回答.\"\n",
    "\n",
    "RESEARCH_BASE_SYSTEM = \"\"\"你是一个专注于批判性思维的人工智能研究助理。你的唯一目标是根据给定的文本，撰写出结构严谨、客观公正、广受好评且文笔优美的报告。\"\"\"\n",
    "\n",
    "RESEARCH_TOPIC_SYSTEM = \"你是一位AI研究助理，你的研究课题是：\\n#TOPIC#\\n{topic}\"\n",
    "\n",
    "SEARCH_TOPIC_PROMPT:str = \"\"\"你是一个研究助理，请你为你的研究课题{topic}提供最多2个必要的关键词用于网络搜索。你的回答必须是JSON格式，例如：[\"关键词1\", \"关键词2\"]。\"\"\"\n",
    "\n",
    "SUMMARIZE_SEARCH_PROMPT = \"\"\"### 要求\n",
    "与你的研究课题相关的关键词及搜索结果会在“搜索结果信息”部分显示。\n",
    "### 搜索结果信息\n",
    "{search_results}\n",
    "根据搜索结果，提供最多 {decomposition_nums} 个与你的研究课题相关的查询。\n",
    "请以以下JSON格式回应：[\"query1\", \"query2\", \"query3\", ...]。除了python的list形式的JSON格式不要输出任何其他内容。\n",
    "\"\"\"\n",
    "\n",
    "COLLECT_AND_RANKURLS_PROMPT = \"\"\"### 主题\n",
    "{topic}\n",
    "### 查询\n",
    "{query}\n",
    "### 在线搜索结果\n",
    "{results}\n",
    "### 要求\n",
    "请移除与查询或主题无关的搜索结果。\n",
    "然后，根据链接的可信度对剩余的搜索结果进行排序。\n",
    "如果两个结果的可信度相同，则根据相关性优先级排序。\n",
    "提供排序后结果的索引，采用JSON格式，例如 [0, 1, 3, 4, ...]，不包含其他文字。\n",
    "\"\"\"\n",
    "\n",
    "class CollectLinks(Action):\n",
    "    \"\"\"Action class to collect links from a search engine.\"\"\"\n",
    "\n",
    "    name: str = \"CollectLinks\"\n",
    "    i_context: Optional[str] = None\n",
    "    desc: str = \"Collect links from a search engine.\"\n",
    "    search_func: Optional[Any] = None\n",
    "    search_engine: Optional[SearchEngine] = None\n",
    "    rank_func: Optional[Callable[[list[str]], None]] = None\n",
    "\n",
    "    @model_validator(mode=\"after\")\n",
    "    def validate_engine_and_run_func(self):\n",
    "        if self.search_engine is None:\n",
    "            self.search_engine = SearchEngine.from_search_func(ZhipuAPIWrapper().run)\n",
    "        return self\n",
    "\n",
    "    async def run(\n",
    "        self,\n",
    "        topic: str,\n",
    "        decomposition_nums: int = 4,\n",
    "        url_per_query: int = 4,\n",
    "        system_text: str | None = None,\n",
    "    ) -> dict[str, list[str]]:\n",
    "        \"\"\"Run the action to collect links.\n",
    "\n",
    "        Args:\n",
    "            topic: The research topic.\n",
    "            decomposition_nums: The number of search questions to generate.\n",
    "            url_per_query: The number of URLs to collect per search question.\n",
    "            system_text: The system text.\n",
    "\n",
    "        Returns:\n",
    "            A dictionary containing the search questions as keys and the collected URLs as values.\n",
    "        \"\"\"\n",
    "        system_text = system_text if system_text else RESEARCH_TOPIC_SYSTEM.format(topic=topic)\n",
    "        keywords = await self._aask(SEARCH_TOPIC_PROMPT.format(topic=topic))\n",
    "        try:\n",
    "            keywords = OutputParser.extract_struct(keywords, list)\n",
    "            keywords = TypeAdapter(list[str]).validate_python(keywords)\n",
    "        except Exception as e:\n",
    "            logger.exception(f\"fail to get keywords related to the research topic '{topic}' for {e}\")\n",
    "            keywords = [topic]\n",
    "        results = await asyncio.gather(*(self.search_engine.run(i, as_string=False) for i in keywords))\n",
    "\n",
    "        def gen_msg():\n",
    "            while True:\n",
    "                search_results = \"\\n\".join(\n",
    "                    f\"#### Keyword: {i}\\n Search Result: {j}\\n\" for (i, j) in zip(keywords, results)\n",
    "                )\n",
    "                prompt = SUMMARIZE_SEARCH_PROMPT.format(\n",
    "                    decomposition_nums=decomposition_nums, search_results=search_results\n",
    "                )\n",
    "                yield prompt\n",
    "                remove = max(results, key=len)\n",
    "                remove.pop()\n",
    "                if len(remove) == 0:\n",
    "                    break\n",
    "\n",
    "        model_name = 'gpt-4'\n",
    "        prompt = reduce_message_length(gen_msg(), model_name, system_text, config.llm.max_token)\n",
    "        logger.debug(prompt)\n",
    "        queries = await self._aask(prompt, [system_text])\n",
    "        try:\n",
    "            queries = OutputParser.extract_struct(queries, list)\n",
    "            queries = TypeAdapter(list[str]).validate_python(queries)\n",
    "        except Exception as e:\n",
    "            logger.exception(f\"fail to break down the research question due to {e}\")\n",
    "            queries = keywords\n",
    "        ret = {}\n",
    "        for query in queries:\n",
    "            ret[query] = await self._search_and_rank_urls(topic, query, url_per_query)\n",
    "        return ret\n",
    "\n",
    "    async def _search_and_rank_urls(self, topic: str, query: str, num_results: int = 2) -> list[str]:\n",
    "        \"\"\"Search and rank URLs based on a query.\n",
    "\n",
    "        Args:\n",
    "            topic: The research topic.\n",
    "            query: The search query.\n",
    "            num_results: The number of URLs to collect.\n",
    "\n",
    "        Returns:\n",
    "            A list of ranked URLs.\n",
    "        \"\"\"\n",
    "        max_results = max(num_results * 2, 5)\n",
    "        results = await self.search_engine.run(query, max_results=max_results, as_string=False)\n",
    "        if len(results) == 0:\n",
    "            return []\n",
    "        _results = \"\\n\".join(f\"{i}: {j}\" for i, j in zip(range(max_results), results))\n",
    "        prompt = COLLECT_AND_RANKURLS_PROMPT.format(topic=topic, query=query, results=_results)\n",
    "        logger.debug(prompt)\n",
    "        indices = await self._aask(prompt)\n",
    "        try:\n",
    "            indices = OutputParser.extract_struct(indices, list)\n",
    "            assert all(isinstance(i, int) for i in indices)\n",
    "        except Exception as e:\n",
    "            logger.exception(f\"fail to rank results for {e}\")\n",
    "            indices = list(range(max_results))\n",
    "        results = [results[i] for i in indices]\n",
    "        if self.rank_func:\n",
    "            results = self.rank_func(results)\n",
    "        return [i[\"link\"] for i in results[:num_results]]"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d3a1aa85",
   "metadata": {},
   "source": [
    "需要注意的几个地方是：\n",
    "\n",
    "我们通过 self.search_engine = SearchEngine.from_search_func(ZhipuAPIWrapper().run) 这行代码，把自定义的搜索工具传给Action。\n",
    "在运行reduce_message_length之前，我们需要改一下model_name\n",
    "model_name = 'gpt-4'\n",
    "\n",
    "因为这个函数会利用"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 85,
   "id": "7bbed0cb",
   "metadata": {},
   "outputs": [],
   "source": [
    "# import tiktoken\n",
    "# model_name = \"qwen2.5:7b\"\n",
    "# # 假设 \"qwen2.5:7b\" 使用与 \"gpt-4\" 相同的编码器\n",
    "# # encoding_name = \"gpt-4\"  # 或者其他已知与 \"qwen2.5:7b\" 兼容的编码器名称\n",
    "# encoding = tiktoken.get_encoding(encoding_name)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "025192ef",
   "metadata": {},
   "source": [
    "来计算某个字符串对应多少token。而tiktoken只认得OpenAI家的模型，它不认得我们本地的qwen2.5:7b，会报错，所以我们改一下模型名称，不让他报错。\n",
    "\n",
    "\n",
    "我们现在来试试这个CollectLinks好用不？"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 84,
   "id": "e9c7da62",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "```json\n",
      "[\"专利实施例数量\", \"授权率影响"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "2025-02-26 17:08:46.933 | WARNING  | metagpt.utils.cost_manager:update_cost:49 - Model glm-4-flash not found in TOKEN_COSTS.\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\"]\n",
      "```\n",
      "current_date: 2025-02-26\n",
      "current_date: 2025-02-26\n",
      "[\"专利实施例数量如何影响专利授权率\", \"专利说明书中的实施例对授权率有何影响\", \"专利实施例数量与专利授权质量的关系\", \"专利实施例的详细"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "2025-02-26 17:09:08.996 | WARNING  | metagpt.utils.cost_manager:update_cost:49 - Model glm-4-flash not found in TOKEN_COSTS.\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "程度对授权率的影响\"]\n",
      "current_date: 2025-02-26\n",
      "```json\n",
      "[0, 1, 3, 2, 4, 5, 6]\n",
      "```"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "2025-02-26 17:09:25.634 | WARNING  | metagpt.utils.cost_manager:update_cost:49 - Model glm-4-flash not found in TOKEN_COSTS.\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "current_date: 2025-02-26\n",
      "```json\n",
      "[0, 1, 4, 2, 3, 6, 7]\n",
      "```"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "2025-02-26 17:10:01.970 | WARNING  | metagpt.utils.cost_manager:update_cost:49 - Model glm-4-flash not found in TOKEN_COSTS.\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "current_date: 2025-02-26\n",
      "```json\n",
      "[2, 0, 1, 3"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "2025-02-26 17:10:20.908 | WARNING  | metagpt.utils.cost_manager:update_cost:49 - Model glm-4-flash not found in TOKEN_COSTS.\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      ", 5]\n",
      "```\n",
      "current_date: 2025-02-26\n",
      "```json\n",
      "[0, 1,"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "2025-02-26 17:10:39.796 | WARNING  | metagpt.utils.cost_manager:update_cost:49 - Model glm-4-flash not found in TOKEN_COSTS.\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      " 3, 7]\n",
      "```\n",
      "{'专利实施例数量如何影响专利授权率': ['https://zhuanlan.zhihu.com/p/635969428', 'https://www.110ask.com/kuaiwen/10009713880039775605.html', 'https://zhuanlan.zhihu.com/p/657425833', 'https://www.66law.cn/laws/2875459.aspx'], '专利说明书中的实施例对授权率有何影响': ['https://zhuanlan.zhihu.com/p/635969428', 'https://wenwen.sogou.com/z/q808529523.htm', 'https://wenwen.sogou.com/z/q728455154.htm', 'https://zhuanlan.zhihu.com/p/661628898'], '专利实施例数量与专利授权质量的关系': ['https://zhuanlan.zhihu.com/p/660518632', 'https://www.lunwen881.com/zl/246397.html', 'https://m.makepolo.com/product/100431184329.html', 'https://www.lunwen881.com/zl/209952.html'], '专利实施例的详细程度对授权率的影响': ['https://zhuanlan.zhihu.com/p/635969428', 'https://zhuanlan.zhihu.com/p/660518632', 'https://www.66law.cn/laws/2875459.aspx', 'https://www.zhihu.com/question/589219777']}\n"
     ]
    }
   ],
   "source": [
    "collect_links = CollectLinks()\n",
    "links = await collect_links.run(\"专利实施例数量对于授权率的影响\")\n",
    "print(links)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "178a5d8c",
   "metadata": {},
   "source": [
    "输出了\n",
    "{'申请专利哪些因素影响授权率': ['http://mp.weixin.qq.com/s?__biz=MzAxODAzNjIxMQ==&mid=2650189385&idx=2&sn=3b80b4796f330cc818ed626fb9c3046a',\n",
    "  'https://zhuanlan.zhihu.com/p/660518632'],\n",
    " '哪些因素影响专利申请授权率？': ['http://mp.weixin.qq.com/s?__biz=MzkyMTI0OTA3MQ==&mid=2247512783&idx=3&sn=78ab46ff135f29fef20f3de78ec154bd',\n",
    "  'https://zhuanlan.zhihu.com/p/625021220'],\n",
    " '企业的专利授权率对申报知识产权优势企业有影响吗？': ['http://wap.qikanzj.com/p-37224',\n",
    "  'https://zhengce.qizhidao.com/article-zcview/707160600044830720.html']}\n",
    "  \n",
    "说明这个Action没问题。我们继续配置下一个Action"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 86,
   "id": "8af11735",
   "metadata": {},
   "outputs": [],
   "source": [
    "from metagpt.tools.web_browser_engine import WebBrowserEngine\n",
    "WEB_BROWSE_AND_SUMMARIZE_PROMPT = \"\"\"\n",
    "### 要求\n",
    "利用“参考信息”部分的文本回答问题“{query}”。\n",
    "如果问题无法直接用文本回答，但文本与研究课题相关，请提供一个全面的文本摘要。\n",
    "如果文本完全与研究课题无关，请回复简短文字“不相关”。\n",
    "包含所有相关的事实信息、数字、统计数据等（如果有的话）。\n",
    "### 参考信息\n",
    "{content}\n",
    "\"\"\"\n",
    "RESEARCH_TOPIC_SYSTEM = \"你是一位AI研究助理，你的研究课题是：\\n#TOPIC#\\n{topic}\"\n",
    "\n",
    "RESEARCH_BASE_SYSTEM = \"\"\"你是一个专注于批判性思维的人工智能研究助理。你的唯一目标是根据给定的文本，撰写出结构严谨、客观公正、广受好评且文笔优美的报告。\"\"\"\n",
    "\n",
    "class WebBrowseAndSummarize(Action):\n",
    "    \"\"\"Action class to explore the web and provide summaries of articles and webpages.\"\"\"\n",
    "\n",
    "    name: str = \"WebBrowseAndSummarize\"\n",
    "    i_context: Optional[str] = None\n",
    "    desc: str = \"Explore the web and provide summaries of articles and webpages.\"\n",
    "    browse_func: Union[Callable[[list[str]], None], None] = None\n",
    "    web_browser_engine: Optional[WebBrowserEngine] = None\n",
    "\n",
    "    @model_validator(mode=\"after\")\n",
    "    def validate_engine_and_run_func(self):\n",
    "        if self.web_browser_engine is None:\n",
    "            self.web_browser_engine = WebBrowserEngine.from_browser_config(\n",
    "                self.config.browser,\n",
    "                browse_func=self.browse_func\n",
    "            )\n",
    "        return self\n",
    "\n",
    "    async def run(\n",
    "        self,\n",
    "        url: str,\n",
    "        *urls: str,\n",
    "        query: str,\n",
    "        system_text: str = RESEARCH_BASE_SYSTEM,\n",
    "    ) -> dict[str, str]:\n",
    "        \"\"\"Run the action to browse the web and provide summaries.\n",
    "\n",
    "        Args:\n",
    "            url: The main URL to browse.\n",
    "            urls: Additional URLs to browse.\n",
    "            query: The research question.\n",
    "            system_text: The system text.\n",
    "\n",
    "        Returns:\n",
    "            A dictionary containing the URLs as keys and their summaries as values.\n",
    "        \"\"\"\n",
    "        contents = await self.web_browser_engine.run(url, *urls)\n",
    "        if not urls:\n",
    "            contents = [contents]\n",
    "\n",
    "        summaries = {}\n",
    "        prompt_template = WEB_BROWSE_AND_SUMMARIZE_PROMPT.format(query=query, content=\"{}\")\n",
    "        for u, content in zip([url, *urls], contents):\n",
    "            content = content.inner_text\n",
    "            chunk_summaries = []\n",
    "            for prompt in generate_prompt_chunk(content, prompt_template, \"gpt-4\", system_text, 4096):\n",
    "                logger.debug(prompt)\n",
    "                summary = await self._aask(prompt, [system_text])\n",
    "                if summary in \"不相关。\":\n",
    "                    continue\n",
    "                chunk_summaries.append(summary)\n",
    "\n",
    "            if not chunk_summaries:\n",
    "                summaries[u] = None\n",
    "                continue\n",
    "\n",
    "            if len(chunk_summaries) == 1:\n",
    "                summaries[u] = chunk_summaries[0]\n",
    "                continue\n",
    "\n",
    "            content = \"\\n\".join(chunk_summaries)\n",
    "            prompt = WEB_BROWSE_AND_SUMMARIZE_PROMPT.format(query=query, content=content)\n",
    "            summary = await self._aask(prompt, [system_text])\n",
    "            summaries[u] = summary\n",
    "        return summaries"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "11e3fa7f",
   "metadata": {},
   "source": [
    "上面代码也是从https://github.com/geekan/MetaGPT/blob/main/metagpt/actions/research.py 源码中拷贝的，改了几个地方：\n",
    "proxy=self.config.proxy, 删掉了，因为我们不用梯子，不需要代理。\n",
    "if summary == \"Not relevant.\" 改成了 if summary in \"不相关。\"\n",
    "\n",
    "还有提示词都改成中文了，其他代码不变。\n",
    "\n",
    "然后我们来试一下这个Action好用不？"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "2c254d19",
   "metadata": {},
   "outputs": [],
   "source": [
    "vbs = WebBrowseAndSummarize()\n",
    "url = 'http://mp.weixin.qq.com/s?__biz=MzIyOTg5MTA3MQ==&mid=2247503955&idx=1&sn=fb1056b09a06db4b0d3264ad811f640c'\n",
    "vbs_rst = await vbs.run(url,query=\"专利实施例数量对于授权率的影响\")\n",
    "print(vbs_rst)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "16b2cedb",
   "metadata": {},
   "source": [
    "可见这是一个字典，链接是键，内容是值。\n",
    "\n",
    "然后我们继续来构建ConductResearch这个Action。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "07814b02",
   "metadata": {},
   "outputs": [],
   "source": [
    "CONDUCT_RESEARCH_PROMPT = \"\"\"### 参考信息\n",
    "{content}\n",
    "### 要求\n",
    "请根据上述提供的信息，针对以下主题：“{topic}”，撰写一份详细的研究报告。该报告必须满足以下要求：\n",
    "直接围绕选定的主题展开讨论。\n",
    "确保结构严谨、内容深入，尽可能纳入相关事实和数据。\n",
    "以直观的方式呈现数据和研究结果，如有适用，可以使用特性比较表。\n",
    "报告的字数不少于2,000字，并采用Markdown语法进行格式化，遵循APA风格指南。\n",
    "在报告末尾以APA格式列出所有来源的URL。\n",
    "\"\"\"\n",
    "\n",
    "class ConductResearch(Action):\n",
    "    \"\"\"Action class to conduct research and generate a research report.\"\"\"\n",
    "\n",
    "    def __init__(self, **kwargs):\n",
    "        super().__init__(**kwargs)\n",
    "\n",
    "    async def run(\n",
    "        self,\n",
    "        topic: str,\n",
    "        content: str,\n",
    "        system_text: str = RESEARCH_BASE_SYSTEM,\n",
    "    ) -> str:\n",
    "        \"\"\"Run the action to conduct research and generate a research report.\n",
    "\n",
    "        Args:\n",
    "            topic: The research topic.\n",
    "            content: The content for research.\n",
    "            system_text: The system text.\n",
    "\n",
    "        Returns:\n",
    "            The generated research report.\n",
    "        \"\"\"\n",
    "        prompt = CONDUCT_RESEARCH_PROMPT.format(topic=topic, content=content)\n",
    "        logger.debug(prompt)\n",
    "        self.llm.auto_max_tokens = True\n",
    "        return await self._aask(prompt, [system_text])\n",
    "\n",
    "\n",
    "def get_research_system_text(topic: str, language: str):\n",
    "    \"\"\"Get the system text for conducting research.\n",
    "\n",
    "    Args:\n",
    "        topic: The research topic.\n",
    "        language: The language for the system text.\n",
    "\n",
    "    Returns:\n",
    "        The system text for conducting research.\n",
    "    \"\"\"\n",
    "    return \" \".join((RESEARCH_TOPIC_SYSTEM.format(topic=topic), LANG_PROMPT.format(language=language)))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "738a42f1",
   "metadata": {},
   "source": [
    "这个就是把prompt改成中文的了，其他都是源码的内容。\n",
    "我们再来试试这个Action好用不？"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "bf74f94a",
   "metadata": {},
   "outputs": [],
   "source": [
    "conduct_research = ConductResearch()\n",
    "report = await conduct_research.run(topic=\"专利实施例数量对于授权率的影响\", content=str(vbs_rst))\n",
    "print(report)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "35f33a68",
   "metadata": {},
   "source": [
    "输出了：\n",
    "```markdown\n",
    "# 专利实施例数量对于授权率的影响研究报告\n",
    "\n",
    "## 引言\n",
    "\n",
    "专利是创新成果的重要体现形式之一。随着知识产权保护意识的增强，越来越多的企业和个人开始重视专利申请。然而，在实际操作过程中，许多申请人关心的一个重要问题是：专利实施例的数量是否会影响其最终的授权率？本文旨在探讨这一问题，并通过分析相关文献和数据来揭示专利实施例数量与授权率之间的关系。\n",
    "\n",
    "## 文献综述\n",
    "\n",
    "### 专利授权的基本概念\n",
    "\n",
    "根据《中华人民共和国专利法》的规定，发明创造必须具备新颖性、创造性及实用性才能获得专利权。在实际审查过程中，审查员会综合考虑申请文件中描述的技术方案是否满足上述条件来决定是否授予专利权。\n",
    "\n",
    "### 实施例的作用与重要性\n",
    "\n",
    "实施例是专利说明书中的一个重要组成部分，它通过具体实例详细说明了发明创造的技术方案及其应用方式。实施例不仅有助于提高说明书的可读性和理解度，还能为审查员提供更加直观、具体的参考依据，从而帮助其更好地评估申请文件所描述的技术方案是否满足授权条件。\n",
    "\n",
    "## 研究方法\n",
    "\n",
    "### 数据收集\n",
    "\n",
    "为了进行深入研究，我们从多个公开渠道收集了大量专利申请案例数据。这些数据涵盖了不同技术领域和不同规模的企业提交的专利申请情况，并记录了每项申请中包含的具体实施例数量及其最终的授权结果。\n",
    "\n",
    "### 分析工具与步骤\n",
    "\n",
    "1. **数据清洗**：对原始数据进行预处理，包括去除重复值、修正错误信息等。\n",
    "2. **统计分析**：使用描述性统计方法计算各类专利申请案例的基本特征（如平均实施例数）；通过相关性和回归分析探索实施例数量与授权率之间的关系。\n",
    "3. **可视化展示**：利用图表等形式直观地呈现研究结果。\n",
    "\n",
    "## 研究结果\n",
    "\n",
    "### 实施例数量与授权率的相关性\n",
    "\n",
    "通过对收集到的数据进行统计分析，我们发现实施例数量与专利授权率之间存在显著相关性。具体而言，在其他条件相同的情况下：\n",
    "\n",
    "- 申请文件中包含更多实施例的专利申请更有可能获得授权。\n",
    "- 每增加一个实施例，专利授权的可能性大约提高5%至10%。\n",
    "\n",
    "### 实施例质量的重要性\n",
    "\n",
    "值得注意的是，仅仅增加实施例的数量并不足以保证更高的授权率。高质量、详尽且具有代表性的实施例对于提升授权成功率同样至关重要。例如，在某些情况下，虽然增加了多个实施例，但由于这些新增加的实例缺乏新颖性或与主要技术方案关联度不高，反而可能降低整体申请文件的质量。\n",
    "\n",
    "## 讨论\n",
    "\n",
    "### 专利审查过程中的考量因素\n",
    "\n",
    "在实际审查过程中，审查员会综合考虑多项因素来决定是否授予专利权。除了实施例的数量外，还包括但不限于以下几点：\n",
    "\n",
    "- 技术新颖性和创新性\n",
    "- 实用性与技术方案的可行性\n",
    "- 权利要求书的撰写质量\n",
    "- 说明书的整体清晰度和完整性\n",
    "\n",
    "### 对企业策略的影响\n",
    "\n",
    "上述研究结果对企业制定专利申请策略具有重要指导意义。一方面，增加实施例数量可以提高授权率；但另一方面，必须确保这些新增加的内容具有实际价值并能够有效支持主要技术方案。\n",
    "\n",
    "## 结论与建议\n",
    "\n",
    "综上所述，专利实施例的数量确实会对最终的授权率产生影响。然而，在追求更多实施例的同时，企业还应注重提升其撰写质量和代表性，以确保整个申请文件的质量和完整性。此外，对于代理机构而言，则需更加关注如何平衡提高授权率的需求与保持技术方案创新性的目标。\n",
    "\n",
    "## 参考文献\n",
    "\n",
    "- [http://mp.weixin.qq.com/s?__biz=MzIyOTg5MTA3MQ==&mid=2247503955&idx=1&sn=fb1056b09a06db4b0d3264ad811f640c](http://mp.weixin.qq.com/s?__biz=MzIyOTg5MTA3MQ==&mid=2247503955&idx=1&sn=fb1056b09a06db4b0d3264ad811f640c)\n",
    "\n",
    "---\n",
    "\n",
    "请注意，上述报告是基于提供的参考信息进行的模拟撰写，并未引用实际的研究数据或文献。在正式研究中，应通过查阅相关学术论文、官方统计数据等资源来获取更准确的信息\n",
    "2025-02-12 15:05:02.306 | INFO     | metagpt.utils.cost_manager:update_cost:108 - prompt_tokens: 432, completion_tokens: 948\n",
    "和结论。\n",
    "```\n",
    "最后我们来构建Researcher这个Role。\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "4d45ebf4",
   "metadata": {},
   "outputs": [],
   "source": [
    "import asyncio\n",
    "import re\n",
    "\n",
    "from pydantic import BaseModel\n",
    "\n",
    "from metagpt.const import RESEARCH_PATH\n",
    "from metagpt.logs import logger\n",
    "from metagpt.roles.role import Role, RoleReactMode\n",
    "from metagpt.schema import Message\n",
    "\n",
    "\n",
    "class Report(BaseModel):\n",
    "    topic: str\n",
    "    links: dict[str, list[str]] = None\n",
    "    summaries: list[tuple[str, str]] = None\n",
    "    content: str = \"\"\n",
    "\n",
    "\n",
    "class Researcher(Role):\n",
    "    name: str = \"大卫\"\n",
    "    profile: str = \"研究员\"\n",
    "    goal: str = \"收集信息并进行研究\"\n",
    "    constraints: str = \"保证信息的真实和准确\"\n",
    "    language: str = \"zh-cn\"\n",
    "    enable_concurrency: bool = True\n",
    "\n",
    "    def __init__(self, **kwargs):\n",
    "        super().__init__(**kwargs)\n",
    "        self.set_actions([CollectLinks, WebBrowseAndSummarize, ConductResearch])\n",
    "        self._set_react_mode(RoleReactMode.BY_ORDER.value, len(self.actions))\n",
    "        if self.language not in (\"en-us\", \"zh-cn\"):\n",
    "            logger.warning(f\"The language `{self.language}` has not been tested, it may not work.\")\n",
    "\n",
    "    async def _act(self) -> Message:\n",
    "        logger.info(f\"{self._setting}: to do {self.rc.todo}({self.rc.todo.name})\")\n",
    "        todo = self.rc.todo\n",
    "        msg = self.rc.memory.get(k=1)[0]\n",
    "        if isinstance(msg.instruct_content, Report):\n",
    "            instruct_content = msg.instruct_content\n",
    "            topic = instruct_content.topic\n",
    "        else:\n",
    "            topic = msg.content\n",
    "\n",
    "        research_system_text = self.research_system_text(topic, todo)\n",
    "        if isinstance(todo, CollectLinks):\n",
    "            links = await todo.run(topic, 4, 4)\n",
    "            ret = Message(\n",
    "                content=\"\", instruct_content=Report(topic=topic, links=links), role=self.profile, cause_by=todo\n",
    "            )\n",
    "        elif isinstance(todo, WebBrowseAndSummarize):\n",
    "            links = instruct_content.links\n",
    "            todos = (\n",
    "                todo.run(*url, query=query, system_text=research_system_text) for (query, url) in links.items() if url\n",
    "            )\n",
    "            if self.enable_concurrency:\n",
    "                summaries = await asyncio.gather(*todos)\n",
    "            else:\n",
    "                summaries = [await i for i in todos]\n",
    "            summaries = list((url, summary) for i in summaries for (url, summary) in i.items() if summary)\n",
    "            ret = Message(\n",
    "                content=\"\", instruct_content=Report(topic=topic, summaries=summaries), role=self.profile, cause_by=todo\n",
    "            )\n",
    "        else:\n",
    "            summaries = instruct_content.summaries\n",
    "            summary_text = \"\\n---\\n\".join(f\"url: {url}\\nsummary: {summary}\" for (url, summary) in summaries)\n",
    "            content = await self.rc.todo.run(topic, summary_text, system_text=research_system_text)\n",
    "            ret = Message(\n",
    "                content=\"\",\n",
    "                instruct_content=Report(topic=topic, content=content),\n",
    "                role=self.profile,\n",
    "                cause_by=self.rc.todo,\n",
    "            )\n",
    "        self.rc.memory.add(ret)\n",
    "        return ret\n",
    "\n",
    "    def research_system_text(self, topic, current_task: Action) -> str:\n",
    "        \"\"\"BACKWARD compatible\n",
    "        This allows sub-class able to define its own system prompt based on topic.\n",
    "        return the previous implementation to have backward compatible\n",
    "        Args:\n",
    "            topic:\n",
    "            language:\n",
    "\n",
    "        Returns: str\n",
    "        \"\"\"\n",
    "        return get_research_system_text(topic, self.language)\n",
    "\n",
    "    async def react(self) -> Message:\n",
    "        msg = await super().react()\n",
    "        report = msg.instruct_content\n",
    "        self.write_report(report.topic, report.content)\n",
    "        return msg\n",
    "\n",
    "    def write_report(self, topic: str, content: str):\n",
    "        filename = re.sub(r'[\\\\/:\"*?<>|]+', \" \", topic)\n",
    "        filename = filename.replace(\"\\n\", \"\")\n",
    "        if not RESEARCH_PATH.exists():\n",
    "            RESEARCH_PATH.mkdir(parents=True)\n",
    "        filepath = RESEARCH_PATH / f\"{filename}.md\"\n",
    "        filepath.write_text(content)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e3359aa4",
   "metadata": {},
   "source": [
    "这也是直接拷贝的源码。把英文改成中文而已，其他没变。\n",
    "\n",
    "然后我们再定义一个main函数"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "68a22435",
   "metadata": {},
   "outputs": [],
   "source": [
    "async def main(topic: str, language: str = \"zh-cn\", enable_concurrency: bool = True):\n",
    "    role = Researcher(language=language, enable_concurrency=enable_concurrency)\n",
    "    await role.run(topic)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "10915296",
   "metadata": {},
   "source": [
    "最后去在jupyter notebook中运行这个main函数，就会开始写报告了"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "6b1818d4",
   "metadata": {},
   "outputs": [],
   "source": [
    "await main(\"中小企业大模型应用落地的最佳实践\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c3e0f89a",
   "metadata": {},
   "source": [
    "到此为止我们就完成了metaGPT的调研员这个多动作单智能体的学习。这里需要注意的是：\n",
    "\n",
    "1. 搜索工具换成了智谱的\n",
    "2. prompt换成中文的\n",
    "3. Action里的model_name换成\"gpt-4\"避免报错。\n",
    "4. 每构建完一个工具、动作、角色，都测试一下它好用不？"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "503cfc26",
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.10.11"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
